﻿//(c) Alexandru Ghiondea, 2011
using System.Collections.Generic;
using System.Globalization;

namespace JSONDotNET
{
    public class Lexer
    {
        private string myText;
        private int pos = 0;
        public Lexer(string text)
        {
            myText = text;
        }

        public List<Token> Lex()
        {
            var length = myText.Length;
            var tokens = new List<Token>();
            while (pos < length)
            {
                //ignore any whitespace
                ignoreWhitespace();
                var element = myText[pos];

                if (element == '{')
                {
                    tokens.Add(new Token() { value = myText[pos].ToString(), kind = TokenKind.OpenBrace, pos = pos });
                    pos++;
                }
                else if (element == '}')
                {
                    tokens.Add(new Token() { value = myText[pos].ToString(), kind = TokenKind.CloseBrace, pos = pos });
                    pos++;
                }
                else if (element == ']')
                {
                    tokens.Add(new Token() { value = myText[pos].ToString(), kind = TokenKind.CloseSquareBrace, pos = pos });
                    pos++;
                }
                else if (element == '[')
                {
                    tokens.Add(new Token() { value = myText[pos].ToString(), kind = TokenKind.OpenSquareBrace, pos = pos });
                    pos++;
                }
                else if (element == '\"')
                {
                    tokens.Add(new Token() { value = "\"", pos = pos, kind = TokenKind.DoubleQuote });
                    pos++;
                    tokens.AddRange(lexString()); //all the characters inside the string..
                }
                else if (element == ',')
                {
                    tokens.Add(new Token() { value = "\"", pos = pos, kind = TokenKind.Comma });
                    pos++;
                }
                else if (element == '.')
                {
                    tokens.Add(new Token() { value = myText[pos].ToString(), kind = TokenKind.Dot, pos = pos });
                    pos++;
                }
                else if (element == 't')
                {
                    //can this a true token?
                    //it could spell true
                    if (myText.Substring(pos, 4) == "true")
                    {
                        tokens.Add(new Token() { value = "true", kind = TokenKind.True, pos = pos });
                        pos += 4;
                    }
                    else
                    {
                        tokens.Add(new Token() { value = myText[pos].ToString(), kind = TokenKind.Character, pos = pos });
                        pos++;
                    }
                }
                else if (element == 'f')
                {
                    //can this a true token?
                    //it could spell true
                    if (myText.Substring(pos, 5) == "false") //so that we don't have trues or stuff like that
                    {
                        tokens.Add(new Token() { value = "false", kind = TokenKind.False, pos = pos });
                        pos += 5;
                    }
                    else
                    {
                        tokens.Add(new Token() { value = myText[pos].ToString(), kind = TokenKind.Character, pos = pos });
                        pos++;
                    }
                }
                else if (element == 'n')
                {
                    //can this a true token?
                    //it could spell true
                    if (myText.Substring(pos, 4) == "null")
                    {
                        tokens.Add(new Token() { value = "null", kind = TokenKind.Null, pos = pos });
                        pos += 4;
                    }
                    else
                    {
                        tokens.Add(new Token() { value = myText[pos].ToString(), kind = TokenKind.Character, pos = pos });
                        pos++;
                    }
                }
                else if ((element == 'e' || element == 'E') && (myText[pos + 1] == '+' || myText[pos + 1] == '-' || char.IsNumber(myText[pos + 1])))
                {
                    //e is an exponent
                    tokens.Add(new Token() { kind = TokenKind.Exponent, pos = pos, value = element.ToString() });
                    pos++;
                }
                else if (element == '-')
                {
                    tokens.Add(new Token() { kind = TokenKind.Minus, pos = pos, value = "-" });
                    pos++;
                }
                else if (element == '+')
                {
                    tokens.Add(new Token() { kind = TokenKind.Plus, pos = pos, value = "+" });
                    pos++;
                }
                else if (element == '\\')
                {
                    //we read the next one.
                    pos++;

                    //Validate that we have the right escaped character
                    List<char> expectedEscapedChars = new List<char>() { 'n', 'f', 'r', 't', 'b', 'u', 'N', 'F', 'R', 'T', 'B', 'U' };
                    if (!expectedEscapedChars.Contains(myText[pos]))
                        throw new LexerException(string.Format("Unexpected escaped character '{0}'", myText[pos]), pos);

                    tokens.Add(new Token() { value = myText[pos].ToString(), pos = pos - 1, kind = TokenKind.EscapedCharacter });
                    pos++;
                }
                else if (element == ':')
                {
                    tokens.Add(new Token() { value = ":", pos = pos, kind = TokenKind.Colon });
                    pos++;
                }
                else if (char.IsNumber(element))
                {
                    tokens.Add(new Token() { value = element.ToString(), pos = pos, kind = TokenKind.Digit });
                    pos++;
                }
                else
                {
                    tokens.Add(new Token() { value = myText[pos].ToString(), pos = pos - 1, kind = TokenKind.Character });
                    pos++;
                }

                //ignore any whitespace
                ignoreWhitespace();
            }

            return tokens;
        }

        private List<Token> lexString()
        {
            var toks = new List<Token>();
            //we need to lex the chars until the next double quote
            bool escaped = false;
            while (pos < myText.Length && myText[pos] != '\"')
            {
                if (myText[pos] == '\\')
                {
                    //the next char is the escaped character
                    pos++;
                    escaped = true;
                }
                if (escaped)
                {
                    char ch;
                    if (myText[pos] == 'n')
                        ch = '\n';
                    else if (myText[pos] == 'b')
                        ch = '\b';
                    else if (myText[pos] == 'f')
                        ch = '\f';
                    else if (myText[pos] == 'r')
                        ch = '\r';
                    else if (myText[pos] == 't')
                        ch = '\t';
                    else if (myText[pos] == 'u')
                    {
                        //we need to read the next 4 numbers.
                        var digits = myText.Substring(pos + 1, 4);
                        if (!validateAreHEXDigits(digits))
                            throw new LexerException(string.Format("The specified escaped unicode character ('{0}') has unexpected characters in it",digits), pos);

                        //Parse the 4 digits as a hex number and conver that number to a string.
                        ch = (char)int.Parse(digits, NumberStyles.HexNumber);
                        pos += 4; //we only advance the 4 digits. the u is advanced outside this if stmt
                    }
                    else if (myText[pos] == '\"' || myText[pos] == '\\' || myText[pos] == '/')
                    {
                        //for stuff like: " \ /
                        ch = myText[pos];
                    }
                    else
                    {
                        //we made a mistake... there was no escaped character...
                        //we add the actual \ character AND the character after it.
                        toks.Add(new Token() { kind = TokenKind.Character, pos = pos - 1, value = "\\" });
                        ch = myText[pos];
                    }
                    toks.Add(new Token() { kind = TokenKind.Character, pos = pos, value = ch.ToString() });
                    escaped = false;
                }
                else
                {
                    toks.Add(new Token() { kind = TokenKind.Character, pos = pos, value = myText[pos].ToString() });
                }
                pos++;
            }
            toks.Add(new Token() { kind = TokenKind.DoubleQuote, pos = pos, value = myText[pos].ToString() });
            pos++;

            return toks;
        }

        /// <summary>
        /// Checks to see if all the digits in the string are part of a HEX number.
        /// </summary>
        /// <param name="digits"></param>
        /// <returns></returns>
        private bool validateAreHEXDigits(string digits)
        {
            //TODO: Make this list into a singleton object
            List<char> hexDigits = new List<char>() { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'a', 'B', 'b', 'C', 'c', 'D', 'd', 'E', 'e', 'F', 'f' };
            for (int i = 0; i < 4; i++)
            {
                if (!hexDigits.Contains(digits[i]))
                    return false;
            }
            return true;
        }

        private void ignoreWhitespace()
        {
            while (pos < myText.Length && char.IsWhiteSpace(myText[pos]))
            {
                pos++;
            }
        }
    }
}
